/* svnNotifierDaemon.c
 *
 * A self contained daemon that handles notifications to ConArtist
 * servers of changes in subversion repositories.
 */

/*
 * Licensed to the ConArtist Development Team (CADT).
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 * The CADT licenses this file to you under the Apache License, 
 * Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <string.h>

#include <libxml/tree.h>
#include <libxml/parser.h>

#include "svnNotifier.h"

#include <svn_path.h>

static apr_pool_t *daemonPool = NULL;
static apr_pollset_t *pollset = NULL;
static struct interestedParty *suiters = NULL;
static apr_thread_mutex_t *suiterLock = NULL;
static apr_table_t *config = NULL;
static apr_table_t *mappings = NULL;

typedef void (*socketFunction)(apr_socket_t *, apr_pool_t *);

#define XML_CMPNAME(a, b)  xmlStrcmp(a->name, (xmlChar *)b)

static void reportError(const char *msg, apr_status_t rv, 
                        apr_pool_t *pool)
{
    fprintf(stderr, "%s\nError: %d\n'%s'\n", msg, rv,
            apr_psprintf(pool, "%pm", &rv));
}

static void xmlErrorFunc(void * ctx, const char * msg, ...)
{
}

/*
 * Configuration table functions
 */
static const char *configGetString(const char *key)
{
    return apr_table_get(config, key);
}

static void configSetString(const char *key, const char *value)
{
    apr_table_set(config, key, value);
}

static apr_status_t configGetInt(int *valPtr, const char *key)
{
    const char *val = apr_table_get(config, key);
    *valPtr = 0;
    if (!val)
        return APR_EGENERAL;
    *valPtr = atoi(val);
    return APR_SUCCESS;
}

static void configSetInt(const char *key, int val, apr_pool_t *pool)
{
    char *valStr = apr_itoa(pool, val);
    apr_table_set(config, key, valStr);
}

/*
 * Tell people how to start the daemon
 */
static void usage(const char *exe)
{
    fprintf(stderr, "%s usage\n\t%s [config directory]\n", exe, exe);
    exit(1);
}

/*
 * Create a socket using the parameters given. The socket will be
 * opened and then bound and set to listen on the specified host/port
 * combination. Passing a NULL host will have it listening on all
 * available IP addresses.
 * Presently this is IPv4.
 */
static apr_status_t createSocket(const char *host, apr_port_t port,
                                 socketFunction func,
                                 apr_pool_t *pool)
{
    apr_status_t rv;
    apr_socket_t *s = NULL;
    apr_sockaddr_t *sockAddr = NULL;
    apr_pollfd_t socketPoll;

    rv = apr_socket_create(&s, APR_INET, SOCK_STREAM, APR_PROTO_TCP,
                           pool);
    if (rv != APR_SUCCESS) {
        reportError("Unable to create socket", rv, pool);
        return rv;
    }

    rv = apr_sockaddr_info_get(&sockAddr, host, APR_INET,
                               port, 0, pool);
    if (rv != APR_SUCCESS) {
        reportError("Unable to get socket info", rv, pool);
        apr_socket_close(s);
        return rv;
    }

    if ((rv = apr_socket_bind(s, sockAddr)) != APR_SUCCESS ||
        (rv = apr_socket_listen(s, 5)) != APR_SUCCESS) {
        reportError("Unable to bind or listen to socket", rv, pool);
        apr_socket_close(s);
        return rv;
    }

    socketPoll.desc_type = APR_POLL_SOCKET;
    socketPoll.reqevents = APR_POLLIN;
    socketPoll.desc.s = s;
    socketPoll.client_data = func;
    rv = apr_pollset_add(pollset, &socketPoll);
    if (rv != APR_SUCCESS) {
        reportError("Unable to add socket to pollset", rv, pool);
        return rv;
    }
    return APR_SUCCESS;
}

/* To send a notification message we simply connect and then close
 * the socket. This can be adjusted later if required :-)
 */
static void notifyInterestedParty(struct interestedParty *ip, 
                                  apr_pool_t *pool)
{
    apr_socket_t *sock;
    apr_status_t rv;

    rv = apr_socket_create(&sock, APR_INET, SOCK_STREAM, APR_PROTO_TCP,
                           pool);
    if (rv == APR_SUCCESS) {
        rv = apr_socket_connect(sock, ip->sa);
        if (rv != APR_SUCCESS)
            nLog(N_ERR, N_HANDLER, pool, "Unable to connect to %pI",
                 ip->sa);
        else
            nLog(N_INFO, N_HANDLER, pool, "Notified %pI of update", ip->sa);
        apr_socket_close(sock);
    } else
        nLog(N_ERR, N_HANDLER, pool, "Unable to create a socket to notify "
             "%pI", ip->sa);
}

/* Based on code in svnlook.c */
static svn_error_t *checkSuiters(svn_repos_node_t *node, const char *repo,
                                 apr_pool_t *pool)
{
    char *full_path = NULL;
    apr_pool_t *subpool = svn_pool_create(pool);
    svn_repos_node_t *tmp_node = node->child;
    struct interestedParty *ip = suiters;

    /* Make sure we have a node and it's a directory */
    if (!node || node->kind != svn_node_dir)
        return SVN_NO_ERROR;

    full_path = svn_path_join(repo, node->name, subpool);

    nLog(N_DEBUG, N_HANDLER, pool, "Checking repo path '%s' for notifications",
         full_path);

    for (; ip; ip = ip->next) {
        const char *ck = apr_table_get(ip->repos, full_path);
        if (ck)
            notifyInterestedParty(ip, subpool);
    }

    if (! tmp_node) {
//        nLog(N_DEBUG, N_HANDLER, pool, "%s has no child nodes", full_path);
        return SVN_NO_ERROR;
    }

    SVN_ERR(checkSuiters(tmp_node, full_path, subpool));
    while (tmp_node->sibling) {
        svn_pool_clear(subpool);
        tmp_node = tmp_node->sibling;
        full_path = svn_path_join(repo, tmp_node->name, subpool);
        SVN_ERR(checkSuiters(tmp_node, full_path, subpool));
    }
    svn_pool_destroy(subpool);
    return SVN_NO_ERROR;
}

static void processLocal(apr_socket_t *acceptedSocket, apr_pool_t *pool)
{
    char *buffer = (char *)apr_pcalloc(pool, 1024);
    apr_size_t bytesRead = 1024;
    apr_status_t rv;
    char *repo = NULL, *revStr = NULL;
    svn_repos_node_t *tree = NULL;

    nLog(N_DEBUG, N_HANDLER, pool, "Accepted a local notification");

    /* Read data from socket, then close it */
    rv = apr_socket_recv(acceptedSocket, buffer, &bytesRead);
    apr_socket_close(acceptedSocket);

    /* If there are no servers asking for notifications, then don't
     * process any further.
     */
    if (!suiters) {
        nLog(N_INFO, N_HANDLER, pool, "No suiters, ignoring notification\n");
        return;
    }

    if (bytesRead > 0) {
        xmlDoc *doc = xmlParseDoc((xmlChar *)buffer);
        if (doc) {
            xmlNode *root = xmlDocGetRootElement(doc);
            xmlNode *node = NULL;
            if (XML_CMPNAME(root, "update") == 0) {
               for (node = root->children; node; node = node->next) {
                    if (XML_CMPNAME(node, "repos") == 0)
                        repo = apr_pstrdup(pool, 
                                           (char *)xmlNodeGetContent(node));
                    if (XML_CMPNAME(node, "rev") == 0)
                        revStr = apr_pstrdup(pool, 
                                            (char *)xmlNodeGetContent(node));
                    
                }
            }
            xmlFreeDoc(doc);
        }
    }

    nLog(N_INFO, N_HANDLER, pool, "Repo '%s' is now at revision %s", 
         repo, revStr);

    if (getSVNDirsChanged(&tree, repo, atoi(revStr), pool) == SVN_NO_ERROR) {
        checkSuiters(tree, repo, pool);
    } else
        nLog(N_WARN, N_HANDLER, pool, "Unable to get a tree for repo path "
             "%s, revision %s\n", repo, revStr);

}

static const char *findMapping(const char *url, apr_pool_t *pool)
{
    const char *fsPath = NULL;
    char *origStr = NULL, *slash = NULL;

    if (apr_is_empty_table(mappings)) {
        nLog(N_WARN, N_HANDLER, pool, "There are no mappings available, "
             "unable to map '%s'", url);
        return NULL;
    }


    /* Is it a URL? Very simplistic check... */
    if (strstr(url, "//") == NULL) {
        nLog(N_WARN, N_HANDLER, pool, "Repo '%s' does not appear to be a URL",
             url);
        return NULL;
    }

    fsPath = apr_table_get(mappings, url);
    if (fsPath)
        return fsPath;

    origStr = apr_pstrdup(pool, url);
    while ((slash = strrchr(origStr, '/'))) {
        *slash = '\0';
        fsPath = apr_table_get(mappings, origStr);
        if (fsPath) {
            apr_size_t oLen = strlen(origStr);
            apr_filepath_merge((char **)&fsPath, fsPath, &url[oLen + 1],
                               0, pool); 
            return fsPath;
        }
    }
    return fsPath;
}

static void processRemote(apr_socket_t *acceptedSocket, apr_pool_t *pool)
{
    char *buffer = (char *)apr_palloc(pool, 1024);
    apr_size_t bytesRead = 1024;
    apr_sockaddr_t *them = NULL;
    apr_status_t rv;
    struct interestedParty *ip = NULL;

    apr_socket_addr_get(&them, APR_REMOTE, acceptedSocket);
    apr_socket_timeout_set(acceptedSocket, apr_time_from_sec(3));
    nLog(N_DEBUG, N_HANDLER, pool, "Accepted remote connection from %pI",
         them);

    rv = makeInterestedParty(&ip, them, pool);
    if (rv == APR_SUCCESS) {
        struct interestedParty *oldIp;
        char *id = NULL;
        apr_table_t *newRepos = apr_table_make(pool, 0);

        if (checkInterestedParty(suiters, ip, &oldIp)) {
            nLog(N_WARN, N_HANDLER, pool, "Duplicate server asking for "
                 "notifications? Removing current repos list");
            apr_table_clear(oldIp->repos);
            ip = oldIp;
        } 

        /* Did the request have any data associated with it? */
        rv = apr_socket_recv(acceptedSocket, buffer, &bytesRead);
        if (bytesRead > 0) {
            /* TODO - handle multiple reads... */
            xmlDoc *doc = xmlParseDoc((xmlChar *)buffer);
            if (doc) {
                xmlNode *root = xmlDocGetRootElement(doc);
                if (XML_CMPNAME(root, "svnNotifications") == 0) {
                    xmlNode *node = NULL;
                    for (node = root->children; node; node = node->next) {
                        if (XML_CMPNAME(node, "id") == 0) {
                            id = (char *)xmlNodeGetContent(node);
                            continue;
                        }
                        if (XML_CMPNAME(node, "repo") == 0) {
                            char *url = (char *)xmlNodeGetContent(node);
                            const char *fsPath = findMapping(url, pool);
                            if (fsPath)
                                apr_table_set(newRepos, fsPath, url);
                            xmlFree(url);
                        }
                    }
                }
                xmlFreeDoc(doc);
            }
        }

        if (oldIp) {
            if (!id) {
                nLog(N_WARN, N_HANDLER, pool, "No ID supplied for "
                     "existing server notifcations");
                apr_socket_close(acceptedSocket);
                return;
            }    
            if (strcmp((char *)oldIp->uniqueId, id) != 0) {
                nLog(N_WARN, N_HANDLER, pool, "ID supplied does not match "
                     "existing server ID");
                apr_socket_close(acceptedSocket);
                return;
            }
            apr_table_clear(oldIp->repos);
            oldIp->repos = newRepos;
            nLog(N_INFO, N_HANDLER, pool, "Updated repository list for %pI",
                 them);
        } else {
            ip->repos = newRepos;

            apr_thread_mutex_lock(suiterLock);
            if (!suiters)
                suiters = ip;
            else {
                struct interestedParty *iip = suiters;
                while (iip->next)
                    iip = iip->next;
                iip->next = ip;
            }
            apr_thread_mutex_unlock(suiterLock);

            nLog(N_INFO, N_HANDLER, pool, "Added notification record for %pI",
                 them);

            bytesRead = 32;
            apr_socket_send(acceptedSocket, (char *)ip->uniqueId, &bytesRead);
        }
    }
    apr_socket_close(acceptedSocket);
}

/* We listen on a local socket for notifications of changes via the
 * svnNotifierClient.
 */
apr_status_t setupLocalSocket(apr_pool_t *pool)
{
    const char *host = configGetString("localHost");
    int port = 0;

    configGetInt(&port, "localPort");
    return createSocket(host, (apr_port_t)port, &processLocal, pool);
}

/* We listen on port 7543 for ConArtist servers telling us they have an
 * interest in one or more repositories on this server. We capture the
 * the port they use for the request and respond on that port 
 * subsequently.
 */
apr_status_t setupListener(apr_pool_t *pool)
{
    const char *host = configGetString("remoteHost");
    int port = 0;
    configGetInt(&port, "remotePort");
    return createSocket(host, (apr_port_t)port, &processRemote, pool);
}

static void readConfigFile(const char *confDir, apr_pool_t *pool)
{
    apr_finfo_t info;
    char *fn = NULL;
    const char *ck = NULL;
    xmlDoc *doc = NULL;
    xmlNode *root = NULL;
    xmlNode *node = NULL;

    if (apr_stat(&info, confDir, APR_FINFO_TYPE, pool) != APR_SUCCESS) {
        printf("Trying to create directory '%s'\n", confDir);
        if (mkdir(confDir, S_IRWXU) != 0) {
            fprintf(stderr, "Unable to create configuration directory '%s'\n",
                    confDir);
            return;
        }
    } else if (info.filetype != APR_DIR) {
        fprintf(stderr, "'%s' isn't a directory!\n", confDir);
        return;
    }

    /* Insert default values... */
    configSetInt("remotePort", SVN_NOTIFY_REMOTE, pool);
    configSetString("localHost", "127.0.0.1");
    configSetInt("localPort", SVN_NOTIFY_LOCAL, pool);
    configSetString("logFilename", SVN_NOTIFY_LOG_NAME);
    apr_table_set(config, "configDirectory", confDir);

    apr_filepath_merge(&fn, confDir, "config", 0, pool);

    doc = xmlReadFile(fn, NULL, 0);
    if (!doc) {
        fprintf(stderr, "Unable to process '%s'\n", fn);
        return;
    }

    root = xmlDocGetRootElement(doc);
    if (!root) {
        xmlFreeDoc(doc);
        return;
    }
    if (XML_CMPNAME(root, "notifierConfig") != 0) {
        nLog(N_WARN, N_SETUP, pool, "%s isn't a config file", fn);
        xmlFreeDoc(doc);
        return;
    }

    for (node = root->children; node; node = node->next) {
        xmlChar *value = xmlNodeGetContent(node);
        configSetString((char *)node->name, (char *)value);
        xmlFree(value);
    }

    if ((ck = configGetString("logLevel")))
        nLogLevel(ck);

    xmlFreeDoc(doc);
    return;
}

/* This called after readConfigFile() so the configuration directory
 * should exist.
 */
static void readMappingFile(apr_pool_t *pool)
{
    const char *confDir = configGetString("configDirectory");
    char *fn = NULL;
    xmlDoc *doc = NULL;
    xmlNode *root = NULL, *node = NULL;

    fn = (char *)configGetString("mappingFile");
    if (fn == NULL) {
        apr_filepath_merge(&fn, confDir, "mapping", 0, pool);
        if (!fn) {
            nLog(N_WARN, N_SETUP, pool, "Unable to create mapping filename "
                 "for directory '%s'", confDir);
            return;
        }
    }

    doc = xmlReadFile(fn, NULL, 0);
    if (!doc) {
        nLog(N_WARN, N_SETUP, pool, "Mapping file '%s' doesn't exist", fn);
        return;
    }

    root = xmlDocGetRootElement(doc);
    if (!root) {
        xmlFreeDoc(doc);
        return;
    }

        for (node = root->children; node; node = node->next) {
            xmlNode *dNode;
            char *remote = NULL, *local = NULL;
            if(XML_CMPNAME(node, "svnMapping"))
                continue;
            for (dNode = node->children; dNode; dNode = dNode->next) {
                if (XML_CMPNAME(dNode, "remote") == 0)
                    remote = (char *)xmlNodeGetContent(dNode);
                if (XML_CMPNAME(dNode, "local") == 0)
                    local = (char *)xmlNodeGetContent(dNode);
            }
            if (remote && local) {
                nLog(N_DEBUG, N_SETUP, pool, "Added mapping URL '%s' to '%s'",
                     remote, local);
                apr_table_set(mappings, remote, local);
            }
            /* do we need to free memory from xmlNodeGetContent here? */
        }
        xmlFreeDoc(doc);

}

/* Checks that our configuration is complete and we have enough
 * information to proceed...
 * Returns 1 if we can, 0 if we can't.
 */
static int canWeGoOn(void)
{
    const char *settings[] = {
        "localPort",
        "remotePort",
        "configDirectory",
    };
    int i;

    for (i = 0; i < sizeof(settings) / sizeof(char *); i++) {
        const char *ck = configGetString(settings[i]);
        if (!ck) {
            fprintf(stderr, "%s has not been set!\n", settings[i]);
            return 0;
        }
    }
    return 1;
}

static void openLog(apr_pool_t *pool)
{
    char *path = NULL;
    const char *logfn = configGetString("logFilename");
    if (!logfn)
        return;
    apr_filepath_merge(&path, configGetString("configDirectory"), logfn, 
                       0, pool);
    if (path) {
        nLogOpen(path);
        nLog(N_INFO, N_SETUP, pool, "Log opened");
    }
}

int main(int argc, char **argv)
{
    apr_uid_t uid;
    apr_status_t rv = 1;
    char *userName = NULL, *homeDir = NULL;
    char *confDir = NULL;

    xmlSetGenericErrorFunc(NULL, (xmlGenericErrorFunc)xmlErrorFunc);
    apr_initialize();

    if (apr_pool_create(&daemonPool, NULL) != APR_SUCCESS) {
        fprintf(stderr, "Unable to create a pool\n");
        exit(1);
    }
    if (apr_pollset_create(&pollset, 2, daemonPool, 0) != APR_SUCCESS) {
        fprintf(stderr, "Unable to create required pollset\n");
        goto exit_error;
    }
    config = apr_table_make(daemonPool, 0);
    mappings = apr_table_make(daemonPool, 0);
    apr_thread_mutex_create(&suiterLock, APR_THREAD_MUTEX_DEFAULT, daemonPool);

    fprintf(stdout, "Starting ConArtist Notification daemon...\n");

    apr_uid_current(&uid, NULL, daemonPool);
    apr_uid_name_get(&userName, uid, daemonPool);
    apr_uid_homepath_get(&homeDir, userName, daemonPool);
    apr_filepath_merge(&confDir, homeDir, ".svnNotifier", 0, daemonPool);
    apr_table_set(config, "user", userName);

    if (!confDir && argc == 1) {
        fprintf(stderr, "No config directory - please specify\n");
        usage(argv[0]);
    }

    readConfigFile((argc > 1 ? argv[1] : confDir), daemonPool);
    if (canWeGoOn() == 0) {
        fprintf(stderr, "Unable to continue, exiting...\n");
        usage(argv[0]);
    }

    openLog(daemonPool);
    nLog(N_INFO, N_SETUP, daemonPool, "Running as user '%s'", userName);
    readMappingFile(daemonPool);

    rv = setupLocalSocket(daemonPool);
    if (rv != APR_SUCCESS)
        goto exit_error;

    rv = setupListener(daemonPool);
    if (rv != APR_SUCCESS)
        goto exit_error;

    nLog(N_INFO, N_RUNNING, daemonPool, "Starting listener loop");

    while (1)
    {
        apr_int32_t n = 0;
        const apr_pollfd_t *results = NULL;

        rv = apr_pollset_poll(pollset, 10 * APR_USEC_PER_SEC, &n, &results);

        if (rv == APR_SUCCESS) {
            apr_int32_t i;
            for (i = 0; i < n; i++) {
                apr_socket_t *accepted = NULL;
                socketFunction func = results[i].client_data;
                rv = apr_socket_accept(&accepted, results[i].desc.s, daemonPool);
                if (rv == APR_SUCCESS) {
                    (*func)(accepted, daemonPool);
                }
            }
            continue;
        }
        if (rv != APR_TIMEUP) {
            reportError("Poll failed!", rv, daemonPool);
            break;
        }
    }

    fprintf(stdout, "Shutting down ConArtist Notification daemon\n");
    rv = 0;

exit_error:
    if (suiterLock)
        apr_thread_mutex_destroy(suiterLock);
    if (pollset)
        apr_pollset_destroy(pollset);
    apr_pool_destroy(daemonPool);
    exit(rv);
}
